﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlTypes;
using System.Reflection;
using System.Runtime.Serialization;
using AndreySmirnov.BLToolkitUtils.VSPackage.Model.Config;
using AndreySmirnov.BLToolkitUtils.VSPackage.Model.SchemaProvider;
using BLToolkit.DataAccess;
using BLToolkit.Mapping;
using FieldInfo = AndreySmirnov.BLToolkitUtils.VSPackage.Model.SchemaProvider.FieldInfo;

namespace AndreySmirnov.BLToolkitUtils.VSPackage.CodeGeneration
{

	public class TablesCodeGenerator
	{
		private static readonly CodeTypeReference s_tableNameAttr = new CodeTypeReference(typeof(TableNameAttribute));
		private static readonly CodeTypeReference s_nullableAttr = new CodeTypeReference(typeof(NullableAttribute));
		private static readonly CodeTypeReference s_pkAttr = new CodeTypeReference(typeof(PrimaryKeyAttribute));
		private static readonly CodeTypeReference s_nonUpdatableAttr = new CodeTypeReference(typeof(NonUpdatableAttribute));
		private static readonly CodeTypeReference s_dbTypeAttr = new CodeTypeReference(typeof(DbTypeAttribute));
		private static readonly CodeTypeReference s_mapFieldAttr = new CodeTypeReference(typeof(MapFieldAttribute));
		private static readonly CodeTypeReference s_dataContractAttr = new CodeTypeReference(typeof(DataContractAttribute));
		private static readonly CodeTypeReference s_dataMemberAttr = new CodeTypeReference(typeof(DataMemberAttribute));
		private static readonly CodeTypeReference s_associationAttr = new CodeTypeReference(typeof(AssociationAttribute));

        public CodeCompileUnit GenerateFileForTable(TableInfo table, IList<FieldInfo> fields, 
            IList<ForeignKeyInfo> foreignKeys,
			string rootNamespace,
            BLTCodeGenerationSettings settings,
            Func<string, string, TableInfo> getTableInfoByName,
			CodeDomProvider domProvider)
		{
			var unit = new CodeCompileUnit();
			unit.ReferencedAssemblies.Add("System.dll");
			unit.ReferencedAssemblies.Add("BLToolkit.3.dll");
            var tableNS = rootNamespace;
            if (!table.IsDefaultSchema)
            {
                tableNS += "." + table.SchemaName.CreateValidIdentifier(domProvider);
            }
            var ns = new CodeNamespace(tableNS);
			unit.Namespaces.Add(ns);
		    var className = table.TableName.CreateValidIdentifier(domProvider);
			var decl = new CodeTypeDeclaration(className)
			               {
			                   IsClass = true, 
                               IsPartial = true
			               };
            ns.Types.Add(decl);
			decl.TypeAttributes = TypeAttributes.Public;
			decl.CustomAttributes.Add(
				new CodeAttributeDeclaration(s_tableNameAttr,
					new CodeAttributeArgument(new CodePrimitiveExpression(table.GetFullTableName()))));
            if (settings.GenerateFields && settings.GenerateWCFDataContract)
            {
                decl.CustomAttributes.Add(new CodeAttributeDeclaration(s_dataContractAttr));
            }

			foreach (var fieldInfo in fields)
			{
				CodeTypeMember prop;
                if (settings.GenerateFields)
                {
                    prop = CreateField(decl, fieldInfo.Name.CreateValidIdentifier(domProvider), MapDbTypeToType(fieldInfo.DbType, fieldInfo.IsNullable));
                    if (settings.GenerateWCFDataContract)
                    {
                        prop.CustomAttributes.Add(new CodeAttributeDeclaration(s_dataMemberAttr));
                    }
                }
                else
			    {
			        prop = CreateProperty(decl, fieldInfo.Name.CreateValidIdentifier(domProvider), MapDbTypeToType(fieldInfo.DbType, fieldInfo.IsNullable));
			    }
			    if (fieldInfo.IsPrimaryKey)
				{
					prop.CustomAttributes.Add(new CodeAttributeDeclaration(s_pkAttr));
				}
                if (fieldInfo.IsAutoGenerated)
                {
                    prop.CustomAttributes.Add(new CodeAttributeDeclaration(s_nonUpdatableAttr));
                }

				var dbTypeAttr = new CodeAttributeDeclaration(s_dbTypeAttr,
				                                              new CodeAttributeArgument(
																  new CodeFieldReferenceExpression(
																	  new CodeTypeReferenceExpression(typeof(DbType)),
																	  Enum.GetName(typeof(DbType), fieldInfo.DbType))
																)
															);
				if (fieldInfo.DbTypeSize != 0)
				{
					dbTypeAttr.Arguments.Add(new CodeAttributeArgument(
					                         	new CodePrimitiveExpression(fieldInfo.DbTypeSize)));
				}
				prop.CustomAttributes.Add(dbTypeAttr);
				prop.CustomAttributes.Add(new CodeAttributeDeclaration(
				                          	s_mapFieldAttr,
				                          	new CodeAttributeArgument(
				                          		new CodePrimitiveExpression(fieldInfo.Name))));
				if (fieldInfo.IsNullable)
				{
					prop.CustomAttributes.Add(new CodeAttributeDeclaration(s_nullableAttr));
				}
                prop.CustomAttributes.AddGeneratedCodeAttribute();
			}
            if (settings.GenerateAssociations)
            {
                GenerateAssociations(domProvider, decl, foreignKeys, getTableInfoByName);
            }
			return unit;
		}

	    private static void GenerateAssociations(CodeDomProvider domProvider, CodeTypeDeclaration decl, 
            IEnumerable<ForeignKeyInfo> foreignKeys, Func<string, string, TableInfo> getTableInfoByName)
	    {
	        foreach (var foreignKeyInfo in foreignKeys)
	        {
	            var pkTable = getTableInfoByName(foreignKeyInfo.PrimaryKeyTableName,
	                                             foreignKeyInfo.PrimaryKeyTableSchema);
                var propertyName = GenerateFKPropertyName(foreignKeyInfo, domProvider);
	            var property = CreateProperty(decl,
	                                          propertyName,
	                                          pkTable.GetTableClassNameWithRelativeNamespace(domProvider));
	            property.CustomAttributes.Add(
	                new CodeAttributeDeclaration(s_associationAttr,
	                                             new CodeAttributeArgument("ThisKey",
	                                                                       new CodePrimitiveExpression(
	                                                                           foreignKeyInfo.ForeignKeyColumnName)),
	                                             new CodeAttributeArgument("OtherKey",
	                                                                       new CodePrimitiveExpression(
	                                                                           foreignKeyInfo.PrimaryKeyColumnName))));
                property.CustomAttributes.AddGeneratedCodeAttribute();
	        }
	    }

        private static string GenerateFKPropertyName(ForeignKeyInfo keyInfo, CodeDomProvider domProvider)
        {
            var name = keyInfo.ForeignKeyColumnName.CreateValidIdentifier(domProvider);
            if (name.EndsWith("id", StringComparison.InvariantCultureIgnoreCase))
            {
                //Strip out ending ID
                name = name.Remove(name.Length - 2);
            }
            var pkColumnName = keyInfo.PrimaryKeyColumnName;
            if (pkColumnName.EndsWith("id", StringComparison.InvariantCultureIgnoreCase))
            {
                //Strip out ending ID
                pkColumnName = pkColumnName.Remove(pkColumnName.Length - 2);
            }
            if (pkColumnName == string.Empty)
            {
                pkColumnName = keyInfo.PrimaryKeyTableName.CreateValidIdentifier(domProvider);
            }
            if (name != pkColumnName)
            {
                name = name + pkColumnName;
            }
            else
            {
                var pkTableIdentifier = keyInfo.PrimaryKeyTableName.CreateValidIdentifier(domProvider);
                if (pkTableIdentifier != name)
                {
                    name = string.Format("{0}{1}", name, pkTableIdentifier);
                }
            }
            return name;
        }

	    private static Type MapDbTypeToType(DbType DbType, bool isNullable)
		{
			switch (DbType)
			{
				case DbType.Int64:
					return isNullable ? typeof(long?) : typeof(long);
				case DbType.Binary:
					return typeof (byte[]);
				case DbType.Boolean:
					return isNullable ? typeof(bool?) : typeof(bool);
				case DbType.String:
				case DbType.StringFixedLength:
					return typeof (string);
				case DbType.DateTime:
				case DbType.Date:
				case DbType.Time:
				case DbType.DateTime2:
					return isNullable ? typeof(DateTime?) : typeof(DateTime);
				case DbType.Decimal:
				case DbType.Currency:
					return isNullable ? typeof(decimal?) : typeof(decimal);
				case DbType.Single:
					return isNullable ? typeof(float?) : typeof(float);
				case DbType.Int32:
					return isNullable ? typeof(int?) : typeof(int);
				case DbType.Double:
					return isNullable ? typeof(double?) : typeof(decimal);
				case DbType.Guid:
					return isNullable ? typeof(Guid?) : typeof(Guid);
				case DbType.Int16:
					return isNullable ? typeof(short?) : typeof(short);
				case DbType.Byte:
					return isNullable ? typeof(byte?) : typeof(byte);
				case DbType.Object:
					return typeof(object);
				case DbType.Xml:
					return typeof(SqlXml);
				case DbType.DateTimeOffset:
					return isNullable ? typeof(DateTimeOffset?) : typeof(DateTimeOffset);
				default:
					throw new ArgumentOutOfRangeException("DbType");
			}
			//return null;
		}

		private static string BuildFieldName(string name)
		{
			return string.Format("m_{0}", name);
		}

        private static CodeTypeMember CreateProperty(CodeTypeDeclaration typeDecl, string name, Type type)
		{
			var field = new CodeMemberField(type, BuildFieldName(name));
			typeDecl.Members.Add(field);
			var property = new CodeMemberProperty
			                   {
			                       Name = name,
			                       Type = new CodeTypeReference(type),
// ReSharper disable BitwiseOperatorOnEnumWihtoutFlags
			                       Attributes = MemberAttributes.Public | MemberAttributes.Final
// ReSharper restore BitwiseOperatorOnEnumWihtoutFlags
			                   };
            property.GetStatements.Add(
				new CodeMethodReturnStatement(
					new CodeFieldReferenceExpression(
						new CodeThisReferenceExpression(), field.Name)
					)
				);
			property.SetStatements.Add(
				new CodeAssignStatement(
					new CodeFieldReferenceExpression(
						new CodeThisReferenceExpression(), field.Name),
						new CodePropertySetValueReferenceExpression()
					)
				);
			typeDecl.Members.Add(property);
			return property;
		}

        private static CodeTypeMember CreateProperty(CodeTypeDeclaration typeDecl, string name, string type)
		{
			var field = new CodeMemberField(type, BuildFieldName(name));
			typeDecl.Members.Add(field);
			var property = new CodeMemberProperty
			                   {
			                       Name = name,
			                       Type = new CodeTypeReference(type),
// ReSharper disable BitwiseOperatorOnEnumWihtoutFlags
			                       Attributes = MemberAttributes.Public | MemberAttributes.Final
// ReSharper restore BitwiseOperatorOnEnumWihtoutFlags
			                   };
            property.GetStatements.Add(
				new CodeMethodReturnStatement(
					new CodeFieldReferenceExpression(
						new CodeThisReferenceExpression(), field.Name)
					)
				);
			property.SetStatements.Add(
				new CodeAssignStatement(
					new CodeFieldReferenceExpression(
						new CodeThisReferenceExpression(), field.Name),
						new CodePropertySetValueReferenceExpression()
					)
				);
			typeDecl.Members.Add(property);
			return property;
		}

        private static CodeTypeMember CreateField(CodeTypeDeclaration typeDecl, string name, Type type)
		{
			var field = new CodeMemberField(type, name);
			typeDecl.Members.Add(field);
// ReSharper disable BitwiseOperatorOnEnumWihtoutFlags
            field.Attributes = MemberAttributes.Public | MemberAttributes.Final;
// ReSharper restore BitwiseOperatorOnEnumWihtoutFlags
			return field;
		}
	}
}
